﻿using Microsoft.Practices.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Medhurst.Unity.Configuration.Models;
using System.Yaml;
using System.ComponentModel;
using System.Reflection;
using System.Configuration;

namespace Medhurst.Unity.Configuration
{
    /// <summary>
    /// Performs a conversion between YAML configuration and Unity
    /// registrations.
    /// </summary>
    internal sealed class YamlUnityAdapter
    {
        private readonly YamlConfigLoadFactory factory;

        /// <summary>
        /// Ctor.
        /// </summary>
        internal YamlUnityAdapter(YamlConfigLoadFactory factory)
        {
            this.factory = factory;
        }

        /// <summary>
        /// Perform the fill.
        /// </summary>
        internal void Fill(IUnityContainer unity)
        {
            // walk through each interface in behaviours
            foreach (var key in factory.LoadedBehaviours.List.Keys.Select(x => x.Scalar()))
                ProcessInterface(key, unity);
        }

        /// <summary>
        /// Process a single interface - called by <see cref="Fill"/>.
        /// </summary>
        /// <exception cref="ConfigurationErrorsException">
        /// Either interface in behaviours YAML file is not declared in aliases, or the value specified for the implementation isn't in aliases.
        /// </exception>
        private void ProcessInterface(string interfaceName, IUnityContainer unity)
        {
            // validate interface is known
            if (!factory.LoadedAliases.Interfaces.ContainsKey(interfaceName))
                throw new ConfigurationErrorsException(String.Format("Behaviours YAML file references an unknown interface `{0}`, either declare this in your Aliases YAML file, or fix it in Behaviours. Must now exit.", interfaceName));

            // fetch interface type & validate
            var interfaceType = factory.LoadedAliases.Interfaces[interfaceName];
            if (String.IsNullOrEmpty(interfaceType) || interfaceType.IndexOf(',') == -1)
                throw new ConfigurationErrorsException(String.Format("Value for interface `{0}` in Behaviours YAML file should contain a .NET type reference, it doesn't so must now exit.", interfaceName));

            // now parse the value implementation
            var behaviourDeclaration = factory.LoadedBehaviours.List[interfaceName];
            ProcessImplementationDeclaration(interfaceName, behaviourDeclaration, unity);
        }

        /// <summary>
        /// Process an implementation declaration which could possibly be multiple declarations in 
        /// a sequence. This code was separated into a separate function as it uses some recursion.
        /// </summary>
        private void ProcessImplementationDeclaration(string interfaceName, YamlNode behaviourDeclaration, IUnityContainer unity, bool forceName=false)
        {
            string implementationTypeAlias = null;
            if (behaviourDeclaration.IsMapping())
            {
                // implementation contains additional meta-data, possibly properties
                var behaviourMeta = behaviourDeclaration.AsMapping();
                implementationTypeAlias = behaviourMeta["type"].Scalar();
                ProcessImplementation(interfaceName, implementationTypeAlias, behaviourDeclaration, unity, forceName);
            }
            else if (behaviourDeclaration.IsSequence())
            {
                // process behaviour sequence
                foreach (var subBehaviour in behaviourDeclaration.AsSequence())
                    ProcessImplementationDeclaration(interfaceName, subBehaviour, unity, true);
            }
            else
            {
                // implementation is just a string pointing to your implementation alias
                implementationTypeAlias = behaviourDeclaration.Scalar();
                ProcessImplementation(interfaceName, implementationTypeAlias, null, unity, forceName);
            }
        }

        /// <summary>
        /// Process a single implementation.
        /// </summary>
        /// <exception cref="ConfigurationErrorsException">Unknown implementation</exception>
        private void ProcessImplementation(string interfaceName, string implementationTypeAlias, YamlNode behaviourDeclaration, IUnityContainer unity, bool forceUseName = false)
        {
            // validate implementation type
            if (!factory.LoadedAliases.Implementations.ContainsKey(implementationTypeAlias))
                throw new ConfigurationErrorsException(String.Format("Behaviours YAML file references an unknown implementation `{0}` for interface `{1}`, either declare this in your Aliases YAML file, or fix it in Behaviours. Must now exit.", implementationTypeAlias, interfaceName));

            // fetch implementation .net type / assembly
            var implementationType = factory.LoadedAliases.Implementations[implementationTypeAlias];

            // load this type / assembly
            var resolvedImplementationType = Type.GetType(implementationType);
            var resolvedInterfaceType = Type.GetType(factory.LoadedAliases.Interfaces[interfaceName]);

            // resolve meta-data
            string name = null;

            // if this declaration is part of an array and no name is specified,
            // we should use the type alias as the name so ResolveAll will work
            if (forceUseName) name = implementationTypeAlias;

            var propertyInjectionList = new List<InjectionMember>();
            LifetimeManager lifetime = new PerResolveLifetimeManager();
            if (behaviourDeclaration != null/*no metadata*/ && behaviourDeclaration.IsMapping())
            {
                var bmap = behaviourDeclaration.AsMapping();

                // check for a name
                if (bmap.ContainsKey("name")) name = bmap["name"].Scalar();

                // check for properties
                if (bmap.ContainsKey("properties"))
                {
                    var propList = bmap["properties"].AsSequence();
                    foreach (var prop in propList.Select(x => x.AsMapping()))
                    {
                        var propName = prop["prop"].Scalar();
                        ProcessProperty(propName, prop, interfaceName, implementationTypeAlias, propertyInjectionList, resolvedImplementationType);
                    }
                }

                // constructor injection
                if (bmap.ContainsKey("constructor"))
                    ProcessConstructorInjection(interfaceName, implementationTypeAlias, propertyInjectionList, bmap);

                // get lifetime setting
                if (bmap.ContainsKey("lifetime"))
                    lifetime = GetLifetimeManager(bmap["lifetime"].Scalar());
            }

            // register this type with unity
            if (String.IsNullOrEmpty(name))
                unity.RegisterType(resolvedInterfaceType, resolvedImplementationType, lifetime, propertyInjectionList.ToArray());
            else
                unity.RegisterType(resolvedInterfaceType, resolvedImplementationType, name, lifetime, propertyInjectionList.ToArray());
        }

        private LifetimeManager GetLifetimeManager(string lifetimeStr)
        {
            switch ((lifetimeStr??"").ToLowerInvariant())
            {
                case "thread":
                    return new PerThreadLifetimeManager();
                case "container":
                    return new ContainerControlledLifetimeManager();
                default: // resolve
                    return new PerResolveLifetimeManager();
            }
        }

        private void ProcessConstructorInjection(string interfaceName, string implementationTypeAlias, List<InjectionMember> propertyInjectionList, YamlMapping bmap)
        {
            var ctorList = bmap["constructor"].AsSequence();
            var ctorParamsIn = new List<object>();
            foreach (var ctorArg in ctorList.Select(x => x.AsMapping()))
            {
                var propName = ctorArg["prop"].Scalar();
                try
                {
                    if (ctorArg.ContainsKey("resolve"))
                    {
                        // resolvable value
                        var resolveVal = ctorArg["resolve"].Scalar();
                        var definition = factory.LoadedAliases.Interfaces.Where(x => 0 == String.Compare(resolveVal, x.Key));
                        if (!definition.Any()) throw new ConfigurationErrorsException(String.Format("Problem during ctor re-hydration for implementation {0}, unknown interface {1}", implementationTypeAlias, resolveVal));
                        ctorParamsIn.Add(Type.GetType(definition.First().Value)); // pass type into ctor and unity will do the rest
                    }
                    else
                    {
                        object value = ctorArg["value"].Scalar();
                        if (ctorArg.ContainsKey("conv"))
                        {
                            // convert value
                            value = ConvertValue(propName, interfaceName, implementationTypeAlias, (string)value, ctorArg["conv"].Scalar());

                        }
                        // literal value
                        ctorParamsIn.Add(value);
                    }
                }
                catch (ArgumentException ex)
                {
                    throw new ConfigurationErrorsException("Problem during ctor re-hydration for implementation: " + implementationTypeAlias, ex);
                }
            }
            var ctorParamInjector = new InjectionConstructor(ctorParamsIn.ToArray());
            propertyInjectionList.Add(ctorParamInjector);
        }

        /// <summary>
        /// Process a property and its valid (which may be scalar or a sequence).
        /// </summary>
        /// <exception cref="ConfigurationErrorsException">Property not found in type, but declared in behaviours YAML.</exception>
        private void ProcessProperty(string propName, YamlMapping prop, string interfaceName, string implementationTypeAlias, List<InjectionMember> propertyInjectionList, Type resolvedImplementationType)
        {
            object actualRawValue = null;
            var valueNode = prop["value"];
            
            var propertyMemberInfo = resolvedImplementationType.GetProperty(propName, BindingFlags.Public | BindingFlags.Instance);
            if (propertyMemberInfo == null)
                throw new ConfigurationErrorsException(String.Format("Property `{0}` not found in type `{1}`, check spelling/docs and try again. Will not exit.", propName, implementationTypeAlias));
            var destinationType = propertyMemberInfo.PropertyType;

            if (valueNode.IsSequence())
            {
                // property is an array
                var seq = valueNode.AsSequence();
                var nativeValueList = Array.CreateInstance(destinationType.GetElementType(), seq.Count);
                for (var i = 0; i < seq.Count; i++)
                {
                    object nativeValue = seq[i].Scalar();
                    if (prop.ContainsKey("conv")) nativeValue = ConvertValue(propName, interfaceName, implementationTypeAlias, (string)nativeValue, prop["conv"].Scalar());
                    nativeValue = ChangeType(nativeValue, destinationType);
                    nativeValueList.SetValue(nativeValue, i);
                }
                actualRawValue = nativeValueList;
            }
            else
            {
                // property is a single value
                var strVal = valueNode.Scalar();
                object nativeValue = strVal;
                if (prop.ContainsKey("conv")) nativeValue = ConvertValue(propName, interfaceName, implementationTypeAlias, strVal, prop["conv"].Scalar());
                actualRawValue = Convert.ChangeType(nativeValue, destinationType);
            }
            
            propertyInjectionList.Add(new InjectionProperty(propName, actualRawValue));
        }

        private object ConvertValue(string propertyName, string interfaceName, string implementationAlias, string strVal, string converterAlias)
        {
            if (!factory.LoadedAliases.Converters.ContainsKey(converterAlias))
                throw new ConfigurationErrorsException(String.Format("Behaviour YAML file contains an unknown converter reference `{0}` in declaration `{1}/{2}`, either declare this converter in your Aliases YAML file, or fix it in Behaviours. Must now exit.", converterAlias, interfaceName, implementationAlias));

            var converterType = factory.LoadedAliases.Converters[converterAlias];

            // load this type / assembly
            var resolvedConverterType = Type.GetType(converterType);
            if (!typeof(TypeConverter).IsAssignableFrom(resolvedConverterType))
                throw new ConfigurationErrorsException(String.Format("Type supplied for converter `{0}` (referenced by `{1}/{2}`) in Aliases YAML file is not a Type Converter, looks like you might have the wrong droids! Must now exit.", converterAlias, interfaceName, implementationAlias));

            TypeConverter converter = (TypeConverter)Activator.CreateInstance(resolvedConverterType); // todo: this is correct?

            var res = converter.ConvertFrom(strVal);
            return res;
        }

        private object ChangeType(object x, Type destinationType)
        {
            var finalDestType = destinationType;

            // are we actually being asked to parse a single type into an array?
            if (finalDestType.IsArray && !x.GetType().IsArray) finalDestType = finalDestType.GetElementType();

            return Convert.ChangeType(x, finalDestType);
        }
    }
}
